package org.springframework.security.web.authentication.rememberme;

import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.MessageSource;
import org.springframework.context.MessageSourceAware;
import org.springframework.context.support.MessageSourceAccessor;
import org.springframework.core.log.LogMessage;
import org.springframework.security.authentication.AccountStatusException;
import org.springframework.security.authentication.AccountStatusUserDetailsChecker;
import org.springframework.security.authentication.AuthenticationDetailsSource;
import org.springframework.security.authentication.RememberMeAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.SpringSecurityMessageSource;
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
import org.springframework.security.core.authority.mapping.NullAuthoritiesMapper;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsChecker;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.web.authentication.RememberMeServices;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.security.web.authentication.logout.LogoutHandler;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;

import java.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Base64;

/**
 * @author Dillon
 * @date 2024/7/11
 * @slogan 致敬大师 致敬未来的你
 * @desc 抽象的记住我逻辑处理实现类，定义记住我的公共逻辑
 */
public abstract class AbstractRememberMeServices implements RememberMeServices, InitializingBean, LogoutHandler, MessageSourceAware {

	/**
	 * 记住我的 cookie key
	 */
	public static final String SPRING_SECURITY_REMEMBER_ME_COOKIE_KEY = "remember-me";

	/**
	 * 默认的记住我参数
	 */
	public static final String DEFAULT_PARAMETER = "remember-me";

	/**
	 * 默认记住我保持时间 2周
	 */
	public static final int TWO_WEEKS_S = 1209600;

	/**
	 * 记住我值分割符号
	 */
	private static final String DELIMITER = ":";

	/**
	 * 日志对象
	 */
	protected final Log logger = LogFactory.getLog(getClass());

	/**
	 * 国际化消息类
	 */
	protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();

	/**
	 * 查询登录数据接口
	 */
	private UserDetailsService userDetailsService;

	/**
	 * 用户账号状态校验接口 默认校验 是否过期 是否禁用 是否上锁 凭证是否过期
	 */
	private UserDetailsChecker userDetailsChecker = new AccountStatusUserDetailsChecker();

	private AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource = new WebAuthenticationDetailsSource();

	/**
	 * cookie  key
	 */
	private String cookieName = SPRING_SECURITY_REMEMBER_ME_COOKIE_KEY;

	/**
	 * 标记cookie是否可在哪些子域名共享
	 */
	private String cookieDomain;

	/**
	 * 前端配置记住我key
	 */
	private String parameter = DEFAULT_PARAMETER;

	/**
	 * 是否默认开启记住我 默认不开启
	 */
	private boolean alwaysRemember;

	/**
	 * 记住我 tokenKey
	 */
	private String key;

	/**
	 * 记住我 token存活时间 2周默认
	 */
	private int tokenValiditySeconds = TWO_WEEKS_S;

	/**
	 * 设置cookie是否在https请求时才发送至后端 默认不设置
	 */
	private Boolean useSecureCookie = null;

	/**
	 * 自定义权限处理mapper映射类 ，默认不处理
	 */
	private GrantedAuthoritiesMapper authoritiesMapper = new NullAuthoritiesMapper();

	/**
	 * 构造函数
	 *
	 * @param key                记住我 tokenKey
	 * @param userDetailsService 查询数据源实现类
	 */
	protected AbstractRememberMeServices(String key, UserDetailsService userDetailsService) {
		Assert.hasLength(key, "记住我token key不能为空");
		Assert.notNull(userDetailsService, "UserDetailsService 不能为空");
		this.key = key;
		this.userDetailsService = userDetailsService;
	}

	/**
	 * 后验处理
	 */
	@Override
	public void afterPropertiesSet() {
		Assert.hasLength(this.key, "记住我token key不能为空");
		Assert.notNull(this.userDetailsService, "UserDetailsService 不能为空");
	}

	/**
	 * 自动登录公共方法
	 *
	 * @param request  请求对象
	 * @param response 响应对象
	 * @return 自动登录结果
	 */
	@Override
	public Authentication autoLogin(HttpServletRequest request, HttpServletResponse response) {
		// 从请求对象中获取记住我 key 如果获取不到，返回null 标记登录失败
		String rememberMeCookie = extractRememberMeCookie(request);
		if (rememberMeCookie == null) {
			return null;
		}
		this.logger.debug("执行记住我验证逻辑");
		// 临界值判断
		if (rememberMeCookie.isEmpty()) {
			this.logger.debug("记住我为空");
			// 取消记住我
			cancelCookie(request, response);
			return null;
		}
		try {
			// 获取记住我value并执行切割获取原始拼接数据
			String[] cookieTokens = decodeCookie(rememberMeCookie);
			// 验证自动登录key 如果返回user 则验证成功
			UserDetails user = processAutoLoginCookie(cookieTokens, request, response);
			// 用户校验
			this.userDetailsChecker.check(user);
			this.logger.debug("记住我通过");
			// 构建认证成功token并返回 顶层接口 authentication
			return createSuccessfulAuthentication(request, user);
		} catch (CookieTheftException ex) {
			// 异常取消cookie并抛出异常
			cancelCookie(request, response);
			throw ex;
		} catch (UsernameNotFoundException ex) {
			this.logger.debug("Remember-me login was valid but corresponding user not found.", ex);
		} catch (InvalidCookieException ex) {
			this.logger.debug("Invalid remember-me cookie: " + ex.getMessage());
		} catch (AccountStatusException ex) {
			this.logger.debug("Invalid UserDetails: " + ex.getMessage());
		} catch (RememberMeAuthenticationException ex) {
			this.logger.debug(ex.getMessage());
		}
		// 取消cookie 返回认证失败
		cancelCookie(request, response);
		return null;
	}

	/**
	 * 从请求中获取记住我cookie至
	 *
	 * @param request 请求对象
	 * @return 记住我的值
	 */
	protected String extractRememberMeCookie(HttpServletRequest request) {
		Cookie[] cookies = request.getCookies();
		if (cookies == null) {
			return null;
		}
		for (Cookie cookie : cookies) {
			if (this.cookieName.equals(cookie.getName())) {
				return cookie.getValue();
			}
		}
		return null;
	}

	/**
	 * 登录成功后，封装记住我token 设置 details
	 *
	 * @param request 请求对象
	 * @param user    认证成功的user
	 * @return rememberMe token
	 */
	protected Authentication createSuccessfulAuthentication(HttpServletRequest request, UserDetails user) {
		RememberMeAuthenticationToken auth = new RememberMeAuthenticationToken(this.key, user,
				this.authoritiesMapper.mapAuthorities(user.getAuthorities()));
		auth.setDetails(this.authenticationDetailsSource.buildDetails(request));
		return auth;
	}

	/**
	 * 解密cookie
	 *
	 * @param cookieValue 加密后的字符串
	 * @return 解密后数组
	 * @throws InvalidCookieException 解密异常
	 */
	protected String[] decodeCookie(String cookieValue) throws InvalidCookieException {
		for (int j = 0; j < cookieValue.length() % 4; j++) {
			cookieValue = cookieValue + "=";
		}
		String cookieAsPlainText;
		try {
			cookieAsPlainText = new String(Base64.getDecoder().decode(cookieValue.getBytes()));
		} catch (IllegalArgumentException ex) {
			throw new InvalidCookieException("cookie 不是 base64加密的，cookie值为 '" + cookieValue + "'");
		}
		String[] tokens = StringUtils.delimitedListToStringArray(cookieAsPlainText, DELIMITER);
		for (int i = 0; i < tokens.length; i++) {
			tokens[i] = URLDecoder.decode(tokens[i], StandardCharsets.UTF_8);
		}
		return tokens;
	}

	/**
	 * 加密cookie数组为字符串
	 *
	 * @param cookieTokens cookie数组
	 * @return 加密字符串
	 */
	protected String encodeCookie(String[] cookieTokens) {
		StringBuilder sb = new StringBuilder();
		for (int i = 0; i < cookieTokens.length; i++) {
			sb.append(URLEncoder.encode(cookieTokens[i], StandardCharsets.UTF_8));
			if (i < cookieTokens.length - 1) {
				sb.append(DELIMITER);
			}
		}
		String value = sb.toString();
		sb = new StringBuilder(new String(Base64.getEncoder().encode(value.getBytes())));
		// 小心机，去除base64中 = 号 解密时在拼接上
		while (sb.charAt(sb.length() - 1) == '=') {
			sb.deleteCharAt(sb.length() - 1);
		}
		return sb.toString();
	}

	/**
	 * 登录失败处理逻辑
	 *
	 * @param request  请求对象
	 * @param response 响应对象
	 */
	@Override
	public void loginFail(HttpServletRequest request, HttpServletResponse response) {
		this.logger.debug("记住我认证失败");
		// 删除 cookie 执行失败处理器（子类选择性实现）
		cancelCookie(request, response);
		onLoginFail(request, response);
	}

	/**
	 * 登录失败处理器
	 *
	 * @param request  请求对象
	 * @param response 响应对象
	 */
	protected void onLoginFail(HttpServletRequest request, HttpServletResponse response) {
	}

	/**
	 * 登录成功处理器
	 *
	 * @param request                  请求对象
	 * @param response                 响应对象
	 * @param successfulAuthentication 自动登录认证成功对象
	 */
	@Override
	public void loginSuccess(HttpServletRequest request, HttpServletResponse response,
			Authentication successfulAuthentication) {
		// 如果需要记住我 则执行登录成功处理器
		if (!rememberMeRequested(request, this.parameter)) {
			this.logger.debug("请求不需要记住我");
			return;
		}
		onLoginSuccess(request, response, successfulAuthentication);
	}

	/**
	 * 模板方法 登录成功逻辑
	 *
	 * @param request                  请求都西昂
	 * @param response                 响应对象
	 * @param successfulAuthentication 认证成功封装类
	 */
	protected abstract void onLoginSuccess(HttpServletRequest request, HttpServletResponse response,
			Authentication successfulAuthentication);

	/**
	 * 是否允许默认开启记住我
	 *
	 * @param request   请求对象
	 * @param parameter 前端传入记住我设置值
	 * @return 配置值
	 */
	protected boolean rememberMeRequested(HttpServletRequest request, String parameter) {
		if (this.alwaysRemember) {
			return true;
		}
		String paramValue = request.getParameter(parameter);
		if (paramValue != null) {
			if (paramValue.equalsIgnoreCase("true") || paramValue.equalsIgnoreCase("on")
					|| paramValue.equalsIgnoreCase("yes") || paramValue.equals("1")) {
				return true;
			}
		}
		this.logger.debug(
				LogMessage.format("未发现记住我cookie (前端未设置记住我参数，前端设置参数为 '%s')", parameter));
		return false;
	}

	/**
	 * 模板方法，验证自动登录的cookie 如果验证成功，返回登录的userDetail
	 *
	 * @param cookieTokens 解密后的cookie数组
	 * @param request      请求对象
	 * @param response     响应对象
	 * @return 验证值
	 * @throws RememberMeAuthenticationException 认证异常
	 * @throws UsernameNotFoundException         用户未找到异常
	 */
	protected abstract UserDetails processAutoLoginCookie(String[] cookieTokens, HttpServletRequest request,
			HttpServletResponse response) throws RememberMeAuthenticationException, UsernameNotFoundException;

	/**
	 * 删除cookie 设置 maxAge = 0 前端执行默认去除
	 *
	 * @param request  请求对象
	 * @param response 响应对象
	 */
	protected void cancelCookie(HttpServletRequest request, HttpServletResponse response) {
		this.logger.debug("Cancelling cookie");
		Cookie cookie = new Cookie(this.cookieName, null);
		cookie.setMaxAge(0);
		cookie.setPath(getCookiePath(request));
		if (this.cookieDomain != null) {
			cookie.setDomain(this.cookieDomain);
		}
		cookie.setSecure((this.useSecureCookie != null) ? this.useSecureCookie : request.isSecure());
		response.addCookie(cookie);
	}

	/**
	 * 设置cookie信息
	 *
	 * @param tokens   待加密字符串
	 * @param maxAge   最大存活时间
	 * @param request  请求对象
	 * @param response 响应对象
	 */
	protected void setCookie(String[] tokens, int maxAge, HttpServletRequest request, HttpServletResponse response) {
		// 加密token数组
		String cookieValue = encodeCookie(tokens);
		Cookie cookie = new Cookie(this.cookieName, cookieValue);
		// 存活时间
		cookie.setMaxAge(maxAge);
		// cookie存储path 前端可以看到
		cookie.setPath(getCookiePath(request));
		// 共享子域名
		if (this.cookieDomain != null) {
			cookie.setDomain(this.cookieDomain);
		}
		if (maxAge < 1) {
			cookie.setVersion(1);
		}
		// 是否只允许https
		cookie.setSecure((this.useSecureCookie != null) ? this.useSecureCookie : request.isSecure());
		// 不允许通过脚本发送，只允许客户端发送
		cookie.setHttpOnly(true);
		response.addCookie(cookie);
	}

	/**
	 * 获取cookie设置路径 根据后端配置的上下文而动态设置
	 *
	 * @param request 请求对象
	 * @return cookie path
	 */
	private String getCookiePath(HttpServletRequest request) {
		String contextPath = request.getContextPath();
		return (!contextPath.isEmpty()) ? contextPath : "/";
	}

	/**
	 * 立即登出 消除cookie缓存
	 *
	 * @param request        请求对象
	 * @param response       响应对象
	 * @param authentication 认证成功对象
	 */
	@Override
	public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
		this.logger.debug(LogMessage.of(() -> "Logout of user " + ((authentication != null) ? authentication.getName() : "Unknown")));
		cancelCookie(request, response);
	}

	/**
	 * 设置cookie名称
	 *
	 * @param cookieName cookie名称
	 */
	public void setCookieName(String cookieName) {
		Assert.hasLength(cookieName, "cookie名称 不能为空");
		this.cookieName = cookieName;
	}

	/**
	 * 设置cookie共享子域名
	 *
	 * @param cookieDomain 设置值
	 */
	public void setCookieDomain(String cookieDomain) {
		Assert.hasLength(cookieDomain, "cookie共享子域名不能为空");
		this.cookieDomain = cookieDomain;
	}

	/**
	 * 获取cookie的参数名
	 *
	 * @return key
	 */
	protected String getCookieName() {
		return this.cookieName;
	}

	/**
	 * 设置是否一直启用记住我 默认不启用
	 *
	 * @param alwaysRemember 设置值
	 */
	public void setAlwaysRemember(boolean alwaysRemember) {
		this.alwaysRemember = alwaysRemember;
	}

	/**
	 * 设置的参数key
	 *
	 * @return key
	 */
	public void setParameter(String parameter) {
		Assert.hasText(parameter, "参数名称不能为空");
		this.parameter = parameter;
	}

	/**
	 * 获取设置的参数key
	 *
	 * @return key
	 */
	public String getParameter() {
		return this.parameter;
	}

	/**
	 * 获取查询数据源实现类
	 *
	 * @return 查询数据源实现类
	 */
	protected UserDetailsService getUserDetailsService() {
		return this.userDetailsService;
	}

	/**
	 * 获取session token key
	 *
	 * @return key
	 */
	public String getKey() {
		return this.key;
	}

	/**
	 * 设置cookie存活时间 默认2周
	 *
	 * @param tokenValiditySeconds 设置值
	 */
	public void setTokenValiditySeconds(int tokenValiditySeconds) {
		this.tokenValiditySeconds = tokenValiditySeconds;
	}

	/**
	 * 获取cookie存活时间
	 *
	 * @return 设置值
	 */
	protected int getTokenValiditySeconds() {
		return this.tokenValiditySeconds;
	}

	/**
	 * 设置是否只有https请求才发送cookie 默认不设置
	 *
	 * @param useSecureCookie 设置值
	 */
	public void setUseSecureCookie(boolean useSecureCookie) {
		this.useSecureCookie = useSecureCookie;
	}

	/**
	 * 获取证成功detail构建实现类
	 *
	 * @return 证成功detail构建实现类
	 */
	protected AuthenticationDetailsSource<HttpServletRequest, ?> getAuthenticationDetailsSource() {
		return this.authenticationDetailsSource;
	}

	/**
	 * 设置自定义认证成功detail构建实现类
	 *
	 * @param authenticationDetailsSource detail构建实现类
	 */
	public void setAuthenticationDetailsSource(
			AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource) {
		Assert.notNull(authenticationDetailsSource, "AuthenticationDetailsSource 不能为空");
		this.authenticationDetailsSource = authenticationDetailsSource;
	}

	/**
	 * 设置自定义用户校验实现类
	 *
	 * @param userDetailsChecker 用户校验实现类
	 */
	public void setUserDetailsChecker(UserDetailsChecker userDetailsChecker) {
		this.userDetailsChecker = userDetailsChecker;
	}

	/**
	 * 设置自定义权限转换实现
	 *
	 * @param authoritiesMapper 权限转换实现
	 */
	public void setAuthoritiesMapper(GrantedAuthoritiesMapper authoritiesMapper) {
		this.authoritiesMapper = authoritiesMapper;
	}

	/**
	 * 国际化消息设置
	 *
	 * @param messageSource 国际化消息
	 */
	@Override
	public void setMessageSource(MessageSource messageSource) {
		Assert.notNull(messageSource, "国际化消息不能为空");
		this.messages = new MessageSourceAccessor(messageSource);
	}

}
